

package com.hazelcast.client.impl.clientside;

import com.hazelcast.client.config.*;
import com.hazelcast.client.config.impl.ClientAliasedDiscoveryConfigUtils;
import com.hazelcast.client.impl.ClientExtension;
import com.hazelcast.client.impl.connection.AddressProvider;
import com.hazelcast.client.impl.spi.ClientClusterService;
import com.hazelcast.client.impl.spi.impl.DefaultAddressProvider;
import com.hazelcast.client.impl.spi.impl.TranslateToPublicAddressProvider;
import com.hazelcast.client.impl.spi.impl.discovery.HazelcastCloudDiscovery;
import com.hazelcast.client.impl.spi.impl.discovery.RemoteAddressProvider;
import com.hazelcast.client.impl.spi.impl.discovery.ViridianAddressProvider;
import com.hazelcast.client.properties.ClientProperty;
import com.hazelcast.cluster.Address;
import com.hazelcast.config.DiscoveryConfig;
import com.hazelcast.config.DiscoveryStrategyConfig;
import com.hazelcast.config.SSLConfig;
import com.hazelcast.config.SocketInterceptorConfig;
import com.hazelcast.config.security.StaticCredentialsFactory;
import com.hazelcast.core.LifecycleService;
import com.hazelcast.internal.config.DiscoveryConfigReadOnly;
import com.hazelcast.logging.ILogger;
import com.hazelcast.logging.LoggingService;
import com.hazelcast.nio.SocketInterceptor;
import com.hazelcast.security.ICredentialsFactory;
import com.hazelcast.security.UsernamePasswordCredentials;
import com.hazelcast.spi.discovery.DiscoveryNode;
import com.hazelcast.spi.discovery.impl.DefaultDiscoveryService;
import com.hazelcast.spi.discovery.impl.DefaultDiscoveryServiceProvider;
import com.hazelcast.spi.discovery.integration.DiscoveryMode;
import com.hazelcast.spi.discovery.integration.DiscoveryService;
import com.hazelcast.spi.discovery.integration.DiscoveryServiceProvider;
import com.hazelcast.spi.discovery.integration.DiscoveryServiceSettings;
import com.hazelcast.spi.properties.HazelcastProperties;

import java.util.*;

import static com.hazelcast.client.properties.ClientProperty.DISCOVERY_SPI_ENABLED;
import static com.hazelcast.client.properties.ClientProperty.HAZELCAST_CLOUD_DISCOVERY_TOKEN;
import static com.hazelcast.internal.config.AliasedDiscoveryConfigUtils.allUsePublicAddress;
import static com.hazelcast.internal.util.Preconditions.checkNotNull;
import static java.util.Collections.unmodifiableList;

class ClusterDiscoveryServiceBuilder {

    private final int configsTryCount;
    private final LoggingService loggingService;
    private final HazelcastProperties properties;
    private final ClientExtension clientExtension;
    private final Collection<ClientConfig> configs;
    private final LifecycleService lifecycleService;
    private final AddressProvider externalAddressProvider;
    private final ClientClusterService clusterService;

    ClusterDiscoveryServiceBuilder(int configsTryCount, List<ClientConfig> configs, LoggingService loggingService, AddressProvider externalAddressProvider, HazelcastProperties properties, ClientExtension clientExtension, LifecycleService lifecycleService, ClientClusterService clusterService) {
        this.configsTryCount = configsTryCount;
        this.configs = configs;
        this.loggingService = loggingService;
        this.externalAddressProvider = externalAddressProvider;
        this.properties = properties;
        this.clientExtension = clientExtension;
        this.lifecycleService = lifecycleService;
        this.clusterService = clusterService;
    }

    private static String cloudDiscoveryToken(ClientCloudConfig cloudConfig, String cloudDiscoveryToken) {
        if (cloudConfig.isEnabled()) {
            return cloudConfig.getDiscoveryToken();
        } else {
            return cloudDiscoveryToken;
        }
    }

    public ClusterDiscoveryService build() {
        ArrayList<CandidateClusterContext> contexts = new ArrayList<>();
        for (ClientConfig config : configs) {
            ClientNetworkConfig networkConfig = config.getNetworkConfig();
            SocketInterceptor interceptor = initSocketInterceptor(networkConfig.getSocketInterceptorConfig());
            ICredentialsFactory credentialsFactory = initCredentialsFactory(config);
            if (credentialsFactory == null) {
                credentialsFactory = new StaticCredentialsFactory(new UsernamePasswordCredentials(null, null));
            }
            credentialsFactory.configure(new ClientCallbackHandler(config));
            DiscoveryService discoveryService = initDiscoveryService(config);
            AddressProvider provider;
            if (externalAddressProvider != null) {
                provider = externalAddressProvider;
            } else {
                provider = createAddressProvider(config, discoveryService);
            }

            final SSLConfig sslConfig = networkConfig.getSSLConfig();
            final SocketOptions socketOptions = networkConfig.getSocketOptions();
            contexts.add(new CandidateClusterContext(config.getClusterName(), provider, discoveryService, credentialsFactory, interceptor, clientExtension.createChannelInitializer(sslConfig, socketOptions)));
        }
        return new ClusterDiscoveryService(unmodifiableList(contexts), configsTryCount, lifecycleService);
    }

    private AddressProvider createAddressProvider(ClientConfig clientConfig, DiscoveryService discoveryService) {
        ClientNetworkConfig networkConfig = clientConfig.getNetworkConfig();
        ClientCloudConfig cloudConfig = networkConfig.getCloudConfig();

        List<String> addresses = networkConfig.getAddresses();
        boolean addressListProvided = !addresses.isEmpty();
        boolean awsDiscoveryEnabled = networkConfig.getAwsConfig() != null && networkConfig.getAwsConfig().isEnabled();
        boolean gcpDiscoveryEnabled = networkConfig.getGcpConfig() != null && networkConfig.getGcpConfig().isEnabled();
        boolean azureDiscoveryEnabled = networkConfig.getAzureConfig() != null && networkConfig.getAzureConfig().isEnabled();
        boolean kubernetesDiscoveryEnabled = networkConfig.getKubernetesConfig() != null && networkConfig.getKubernetesConfig().isEnabled();
        boolean eurekaDiscoveryEnabled = networkConfig.getEurekaConfig() != null && networkConfig.getEurekaConfig().isEnabled();
        boolean discoverySpiEnabled = discoverySpiEnabled(networkConfig);
        String cloudDiscoveryToken = properties.getString(HAZELCAST_CLOUD_DISCOVERY_TOKEN);
        if (cloudDiscoveryToken != null && cloudConfig.isEnabled()) {
            throw new IllegalStateException("Ambiguous hazelcast.cloud configuration. " + "Both property based and client configuration based settings are provided for " + "Hazelcast cloud discovery together. Use only one.");
        }
        boolean hazelcastCloudEnabled = cloudDiscoveryToken != null || cloudConfig.isEnabled();
        isDiscoveryConfigurationConsistent(addressListProvided, awsDiscoveryEnabled, gcpDiscoveryEnabled, azureDiscoveryEnabled, kubernetesDiscoveryEnabled, eurekaDiscoveryEnabled, discoverySpiEnabled, hazelcastCloudEnabled);

        if (hazelcastCloudEnabled) {
            String discoveryToken = cloudDiscoveryToken(cloudConfig, cloudDiscoveryToken);
            String cloudUrlBase = properties.getString(HazelcastCloudDiscovery.CLOUD_URL_BASE_PROPERTY);
            String urlEndpoint = HazelcastCloudDiscovery.createUrlEndpoint(cloudUrlBase, discoveryToken);
            int connectionTimeoutMillis = getConnectionTimeoutMillis(networkConfig);
            boolean tpcEnabled = clientConfig.getTpcConfig().isEnabled();
            HazelcastCloudDiscovery cloudDiscovery = new HazelcastCloudDiscovery(urlEndpoint, connectionTimeoutMillis, tpcEnabled);
            return new ViridianAddressProvider(cloudDiscovery);
        } else
            if (networkConfig.getAddresses().isEmpty() && discoveryService != null) {
                return new RemoteAddressProvider(() -> discoverAddresses(discoveryService), usePublicAddress(clientConfig));
            }
        TranslateToPublicAddressProvider toPublicAddressProvider = new TranslateToPublicAddressProvider(networkConfig, properties, loggingService.getLogger(TranslateToPublicAddressProvider.class));
        clusterService.addMembershipListener(toPublicAddressProvider);
        return new DefaultAddressProvider(networkConfig, toPublicAddressProvider);
    }

    private Map<Address, Address> discoverAddresses(DiscoveryService discoveryService) {
        Iterable<DiscoveryNode> discoveredNodes = checkNotNull(discoveryService.discoverNodes(), "Discovered nodes cannot be null!");
        Map<Address, Address> privateToPublic = new HashMap<>();
        for (DiscoveryNode discoveryNode : discoveredNodes) {
            privateToPublic.put(discoveryNode.getPrivateAddress(), discoveryNode.getPublicAddress());
        }
        return privateToPublic;
    }

    private boolean discoverySpiEnabled(ClientNetworkConfig networkConfig) {
        return (networkConfig.getDiscoveryConfig() != null && networkConfig.getDiscoveryConfig().isEnabled()) || Boolean.parseBoolean(properties.getString(DISCOVERY_SPI_ENABLED));
    }

    private boolean usePublicAddress(ClientConfig config) {
        return properties.getBoolean(ClientProperty.DISCOVERY_SPI_PUBLIC_IP_ENABLED) || allUsePublicAddress(ClientAliasedDiscoveryConfigUtils.aliasedDiscoveryConfigsFrom(config.getNetworkConfig()));
    }

    @SuppressWarnings({"checkstyle:booleanexpressioncomplexity", "checkstyle:npathcomplexity"})
    private void isDiscoveryConfigurationConsistent(boolean addressListProvided, boolean awsDiscoveryEnabled, boolean gcpDiscoveryEnabled, boolean azureDiscoveryEnabled, boolean kubernetesDiscoveryEnabled, boolean eurekaDiscoveryEnabled, boolean discoverySpiEnabled, boolean hazelcastCloudEnabled) {
        List<String> enabledDiscoveries = new ArrayList<>();
        int count = 0;
        if (addressListProvided) {
            count++;
            enabledDiscoveries.add("cluster members given explicitly");
        }
        if (awsDiscoveryEnabled) {
            count++;
            enabledDiscoveries.add("AWS discovery");
        }
        if (gcpDiscoveryEnabled) {
            count++;
            enabledDiscoveries.add("GCP discovery");
        }
        if (azureDiscoveryEnabled) {
            count++;
            enabledDiscoveries.add("Azure discovery");
        }
        if (kubernetesDiscoveryEnabled) {
            count++;
            enabledDiscoveries.add("Kubernetes discovery");
        }
        if (eurekaDiscoveryEnabled) {
            count++;
            enabledDiscoveries.add("Eureka discovery");
        }
        if (discoverySpiEnabled) {
            count++;
            enabledDiscoveries.add("Discovery SPI");
        }
        if (hazelcastCloudEnabled) {
            count++;
            enabledDiscoveries.add("Hazelcast Cloud");
        }
        if (count > 1) {
            throw new IllegalStateException("Only one discovery method can be enabled at a time. " + "Keep only one of the following method enabled by removing the others from the configuration, " + "or setting enabled to 'false': " + String.join(",", enabledDiscoveries));
        }
    }

    private DiscoveryService initDiscoveryService(ClientConfig config) {
        // Prevent confusing behavior where the DiscoveryService is started
        // and strategies are resolved but the AddressProvider is never registered
        List<DiscoveryStrategyConfig> aliasedDiscoveryConfigs = ClientAliasedDiscoveryConfigUtils.createDiscoveryStrategyConfigs(config);

        if (!properties.getBoolean(ClientProperty.DISCOVERY_SPI_ENABLED) && aliasedDiscoveryConfigs.isEmpty() && !config.getNetworkConfig().isAutoDetectionEnabled()) {
            return null;
        }

        ILogger logger = loggingService.getLogger(DiscoveryService.class);
        ClientNetworkConfig networkConfig = config.getNetworkConfig();
        DiscoveryConfig discoveryConfig = new DiscoveryConfigReadOnly(networkConfig.getDiscoveryConfig());

        DiscoveryServiceProvider factory = discoveryConfig.getDiscoveryServiceProvider();
        if (factory == null) {
            factory = new DefaultDiscoveryServiceProvider();
        }

        boolean isAutoDetectionEnabled = networkConfig.isAutoDetectionEnabled();
        DiscoveryServiceSettings settings = new DiscoveryServiceSettings().setConfigClassLoader(config.getClassLoader()).setLogger(logger).setDiscoveryMode(DiscoveryMode.Client).setAliasedDiscoveryConfigs(aliasedDiscoveryConfigs).setDiscoveryConfig(discoveryConfig).setAutoDetectionEnabled(isAutoDetectionEnabled);

        DiscoveryService discoveryService = factory.newDiscoveryService(settings);
        if (isAutoDetectionEnabled && isEmptyDiscoveryStrategies(discoveryService)) {
            return null;
        }
        return discoveryService;
    }

    private boolean isEmptyDiscoveryStrategies(DiscoveryService discoveryService) {
        return discoveryService instanceof DefaultDiscoveryService dds && !dds.getDiscoveryStrategies().iterator().hasNext();
    }

    private ICredentialsFactory initCredentialsFactory(ClientConfig config) {
        ClientSecurityConfig securityConfig = config.getSecurityConfig();
        return securityConfig.asCredentialsFactory(config.getClassLoader());
    }

    private SocketInterceptor initSocketInterceptor(SocketInterceptorConfig sic) {
        if (sic != null && sic.isEnabled()) {
            return clientExtension.createSocketInterceptor(sic);
        }
        return null;
    }

    private int getConnectionTimeoutMillis(ClientNetworkConfig networkConfig) {
        int connTimeout = networkConfig.getConnectionTimeout();
        return connTimeout == 0 ? Integer.MAX_VALUE : connTimeout;
    }
}
